/**
 * @author Mat Groves http://matgroves.com/ @Doormat23
 */

/**
 * @class PIXI.WebGLFilterManager
 * @constructor
 */
PIXI.WebGLFilterManager = function ()
{
    /**
     * @property filterStack
     * @type Array
     */
    this.filterStack = [];

    /**
     * @property offsetX
     * @type Number
     */
    this.offsetX = 0;

    /**
     * @property offsetY
     * @type Number
     */
    this.offsetY = 0;
};

PIXI.WebGLFilterManager.prototype.constructor = PIXI.WebGLFilterManager;

/**
 * Initialises the context and the properties.
 *
 * @method PIXI.WebGLFilterManager#setContext
 * @param gl {WebGLContext} the current WebGL drawing context
 */
PIXI.WebGLFilterManager.prototype.setContext = function (gl)
{
    this.gl = gl;
    this.texturePool = [];

    this.initShaderBuffers();
};

/**
 * @method PIXI.WebGLFilterManager#begin
 * @param renderSession {RenderSession}
 * @param buffer {ArrayBuffer}
 */
PIXI.WebGLFilterManager.prototype.begin = function (renderSession, buffer)
{
    this.renderSession = renderSession;
    this.defaultShader = renderSession.shaderManager.defaultShader;

    var projection = this.renderSession.projection;
    this.width = projection.x * 2;
    this.height = -projection.y * 2;
    this.buffer = buffer;
};

/**
 * Applies the filter and adds it to the current filter stack.
 *
 * @method PIXI.WebGLFilterManager#pushFilter
 * @param filterBlock {Object} the filter that will be pushed to the current filter stack
 */
PIXI.WebGLFilterManager.prototype.pushFilter = function (filterBlock)
{
    var gl = this.gl;

    var projection = this.renderSession.projection;
    var offset = this.renderSession.offset;

    filterBlock._filterArea = filterBlock.target.filterArea || filterBlock.target.getBounds();

    // >>> modify by nextht
    filterBlock._previous_stencil_mgr = this.renderSession.stencilManager; // eslint-disable-line camelcase
    this.renderSession.stencilManager = new PIXI.WebGLStencilManager();
    this.renderSession.stencilManager.setContext(gl);
    gl.disable(gl.STENCIL_TEST);

    // <<<  modify by nextht

    /*
     * filter program
     * OPTIMISATION - the first filter is free if its a simple color change?
     */
    this.filterStack.push(filterBlock);

    var filter = filterBlock.filterPasses[0];

    this.offsetX += filterBlock._filterArea.x;
    this.offsetY += filterBlock._filterArea.y;

    var texture = this.texturePool.pop();
    if(!texture)
    {
        texture = new PIXI.FilterTexture(this.gl, this.width * this.renderSession.resolution, this.height * this.renderSession.resolution);
    }
    else
    {
        texture.resize(this.width * this.renderSession.resolution, this.height * this.renderSession.resolution);
    }

    gl.bindTexture(gl.TEXTURE_2D, texture.texture);

    var filterArea = filterBlock._filterArea;// filterBlock.target.getBounds();///filterBlock.target.filterArea;

    var padding = filter.padding;
    filterArea.x -= padding;
    filterArea.y -= padding;
    filterArea.width += padding * 2;
    filterArea.height += padding * 2;

    // cap filter to screen size..
    if(filterArea.x < 0) { filterArea.x = 0; }
    if(filterArea.width > this.width) { filterArea.width = this.width; }
    if(filterArea.y < 0) { filterArea.y = 0; }
    if(filterArea.height > this.height) { filterArea.height = this.height; }

    // gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA,  filterArea.width, filterArea.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
    gl.bindFramebuffer(gl.FRAMEBUFFER, texture.frameBuffer);

    // set view port
    gl.viewport(0, 0, filterArea.width * this.renderSession.resolution, filterArea.height * this.renderSession.resolution);

    projection.x = filterArea.width / 2;
    projection.y = -filterArea.height / 2;

    offset.x = -filterArea.x;
    offset.y = -filterArea.y;

    /*
     * update projection
     * now restore the regular shader..
     * this.renderSession.shaderManager.setShader(this.defaultShader);
     * gl.uniform2f(this.defaultShader.projectionVector, filterArea.width/2, -filterArea.height/2);
     * gl.uniform2f(this.defaultShader.offsetVector, -filterArea.x, -filterArea.y);
     */

    gl.colorMask(true, true, true, true);
    gl.clearColor(0,0,0, 0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    filterBlock._glFilterTexture = texture;
};

/**
 * Removes the last filter from the filter stack and doesn't return it.
 *
 * @method PIXI.WebGLFilterManager#popFilter
 */
PIXI.WebGLFilterManager.prototype.popFilter = function ()
{
    var gl = this.gl;
    var filterBlock = this.filterStack.pop();
    var filterArea = filterBlock._filterArea;
    var texture = filterBlock._glFilterTexture;
    var projection = this.renderSession.projection;
    var offset = this.renderSession.offset;

    if(filterBlock.filterPasses.length > 1)
    {
        gl.viewport(0, 0, filterArea.width * this.renderSession.resolution, filterArea.height * this.renderSession.resolution);

        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);

        this.vertexArray[0] = 0;
        this.vertexArray[1] = filterArea.height;

        this.vertexArray[2] = filterArea.width;
        this.vertexArray[3] = filterArea.height;

        this.vertexArray[4] = 0;
        this.vertexArray[5] = 0;

        this.vertexArray[6] = filterArea.width;
        this.vertexArray[7] = 0;

        gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertexArray);

        gl.bindBuffer(gl.ARRAY_BUFFER, this.uvBuffer);

        // now set the uvs..
        this.uvArray[2] = filterArea.width / this.width;
        this.uvArray[5] = filterArea.height / this.height;
        this.uvArray[6] = filterArea.width / this.width;
        this.uvArray[7] = filterArea.height / this.height;

        gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.uvArray);

        var inputTexture = texture;
        var outputTexture = this.texturePool.pop();
        if(!outputTexture) { outputTexture = new PIXI.FilterTexture(this.gl, this.width * this.renderSession.resolution, this.height * this.renderSession.resolution); }
        outputTexture.resize(this.width * this.renderSession.resolution, this.height * this.renderSession.resolution);

        // need to clear this FBO as it may have some left over elements from a previous filter.
        gl.bindFramebuffer(gl.FRAMEBUFFER, outputTexture.frameBuffer);
        gl.clear(gl.COLOR_BUFFER_BIT);

        gl.disable(gl.BLEND);

        for (var i = 0; i < filterBlock.filterPasses.length - 1; i++)
        {
            var filterPass = filterBlock.filterPasses[i];

            gl.bindFramebuffer(gl.FRAMEBUFFER, outputTexture.frameBuffer);

            // set texture
            gl.activeTexture(gl.TEXTURE0);
            gl.bindTexture(gl.TEXTURE_2D, inputTexture.texture);

            /*
             * draw texture..
             * filterPass.applyFilterPass(filterArea.width, filterArea.height);
             */
            this.applyFilterPass(filterPass, filterArea, filterArea.width, filterArea.height);

            // swap the textures..
            var temp = inputTexture;
            inputTexture = outputTexture;
            outputTexture = temp;
        }

        gl.enable(gl.BLEND);

        texture = inputTexture;
        this.texturePool.push(outputTexture);
    }

    var filter = filterBlock.filterPasses[filterBlock.filterPasses.length - 1];

    this.offsetX -= filterArea.x;
    this.offsetY -= filterArea.y;

    var sizeX = this.width;
    var sizeY = this.height;

    var offsetX = 0;
    var offsetY = 0;

    var buffer = this.buffer;

    // time to render the filters texture to the previous scene
    if(this.filterStack.length === 0)
    {
        gl.colorMask(true, true, true, true);// this.transparent);
    }
    else
    {
        var currentFilter = this.filterStack[this.filterStack.length - 1];
        filterArea = currentFilter._filterArea;

        sizeX = filterArea.width;
        sizeY = filterArea.height;

        offsetX = filterArea.x;
        offsetY = filterArea.y;

        buffer = currentFilter._glFilterTexture.frameBuffer;
    }

    // TODO need to remove these global elements..
    projection.x = sizeX / 2;
    projection.y = -sizeY / 2;

    offset.x = offsetX;
    offset.y = offsetY;

    filterArea = filterBlock._filterArea;

    var x = filterArea.x - offsetX;
    var y = filterArea.y - offsetY;

    /*
     * update the buffers..
     * make sure to flip the y!
     */
    gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);

    this.vertexArray[0] = x;
    this.vertexArray[1] = y + filterArea.height;

    this.vertexArray[2] = x + filterArea.width;
    this.vertexArray[3] = y + filterArea.height;

    this.vertexArray[4] = x;
    this.vertexArray[5] = y;

    this.vertexArray[6] = x + filterArea.width;
    this.vertexArray[7] = y;

    gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertexArray);

    gl.bindBuffer(gl.ARRAY_BUFFER, this.uvBuffer);

    this.uvArray[2] = filterArea.width / this.width;
    this.uvArray[5] = filterArea.height / this.height;
    this.uvArray[6] = filterArea.width / this.width;
    this.uvArray[7] = filterArea.height / this.height;

    gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.uvArray);

    gl.viewport(0, 0, sizeX * this.renderSession.resolution, sizeY * this.renderSession.resolution);

    // bind the buffer
    gl.bindFramebuffer(gl.FRAMEBUFFER, buffer);

    /*
     * set the blend mode!
     * gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
     */

    // set texture
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, texture.texture);

    // >>> modify by nextht
    if (this.renderSession.stencilManager)
    {
        this.renderSession.stencilManager.destroy();
    }
    this.renderSession.stencilManager = filterBlock._previous_stencil_mgr; // eslint-disable-line camelcase
    filterBlock._previous_stencil_mgr = null; // eslint-disable-line camelcase
    if (this.renderSession.stencilManager.count > 0)
    {
        gl.enable(gl.STENCIL_TEST);
    }
    else
    {
        gl.disable(gl.STENCIL_TEST);
    }

    // <<< modify by nextht

    // apply!
    this.applyFilterPass(filter, filterArea, sizeX, sizeY);

    /*
     * now restore the regular shader.. should happen automatically now..
     * this.renderSession.shaderManager.setShader(this.defaultShader);
     * gl.uniform2f(this.defaultShader.projectionVector, sizeX/2, -sizeY/2);
     * gl.uniform2f(this.defaultShader.offsetVector, -offsetX, -offsetY);
     */

    // return the texture to the pool
    this.texturePool.push(texture);
    filterBlock._glFilterTexture = null;
};

/**
 * Applies the filter to the specified area.
 *
 * @method PIXI.WebGLFilterManager#applyFilterPass
 * @param filter {Phaser.Filter} the filter that needs to be applied
 * @param filterArea {Texture} TODO - might need an update
 * @param width {Number} the horizontal range of the filter
 * @param height {Number} the vertical range of the filter
 */
PIXI.WebGLFilterManager.prototype.applyFilterPass = function (filter, filterArea, width, height)
{
    // use program
    var gl = this.gl;
    var shader = filter.shaders[gl.id];

    if(!shader)
    {
        shader = new PIXI.PixiShader(gl);

        shader.fragmentSrc = filter.fragmentSrc;
        shader.uniforms = filter.uniforms;
        shader.init(true);

        filter.shaders[gl.id] = shader;
    }

    // set the shader
    this.renderSession.shaderManager.setShader(shader);

    //    gl.useProgram(shader.program);

    gl.uniform2f(shader.projectionVector, width / 2, -height / 2);
    gl.uniform2f(shader.offsetVector, 0,0);

    if(filter.uniforms.dimensions)
    {
        filter.uniforms.dimensions.value[0] = this.width;// width;
        filter.uniforms.dimensions.value[1] = this.height;// height;
        filter.uniforms.dimensions.value[2] = this.vertexArray[0];
        filter.uniforms.dimensions.value[3] = this.vertexArray[5];// filterArea.height;
    }

    shader.syncUniforms();

    gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
    gl.vertexAttribPointer(shader.aVertexPosition, 2, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, this.uvBuffer);
    gl.vertexAttribPointer(shader.aTextureCoord, 2, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
    gl.vertexAttribPointer(shader.colorAttribute, 2, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);

    // draw the filter...
    gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

    this.renderSession.drawCount++;
};

/**
 * Initialises the shader buffers.
 *
 * @method PIXI.WebGLFilterManager#initShaderBuffers
 */
PIXI.WebGLFilterManager.prototype.initShaderBuffers = function ()
{
    var gl = this.gl;

    // create some buffers
    this.vertexBuffer = gl.createBuffer();
    this.uvBuffer = gl.createBuffer();
    this.colorBuffer = gl.createBuffer();
    this.indexBuffer = gl.createBuffer();

    /*
     * bind and upload the vertexs..
     * keep a reference to the vertexFloatData..
     */
    this.vertexArray = new Float32Array([ 0.0, 0.0,
        1.0, 0.0,
        0.0, 1.0,
        1.0, 1.0 ]);

    gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, this.vertexArray, gl.STATIC_DRAW);

    // bind and upload the uv buffer
    this.uvArray = new Float32Array([ 0.0, 0.0,
        1.0, 0.0,
        0.0, 1.0,
        1.0, 1.0 ]);

    gl.bindBuffer(gl.ARRAY_BUFFER, this.uvBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, this.uvArray, gl.STATIC_DRAW);

    this.colorArray = new Float32Array([ 1.0, 0xFFFFFF,
        1.0, 0xFFFFFF,
        1.0, 0xFFFFFF,
        1.0, 0xFFFFFF ]);

    gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, this.colorArray, gl.STATIC_DRAW);

    // bind and upload the index
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([ 0, 1, 2, 1, 3, 2 ]), gl.STATIC_DRAW);
};

/**
 * Destroys the filter and removes it from the filter stack.
 *
 * @method PIXI.WebGLFilterManager#destroy
 */
PIXI.WebGLFilterManager.prototype.destroy = function ()
{
    var gl = this.gl;

    this.filterStack = null;

    this.offsetX = 0;
    this.offsetY = 0;

    // destroy textures
    for (var i = 0; i < this.texturePool.length; i++)
    {
        this.texturePool[i].destroy();
    }

    this.texturePool = null;

    // destroy buffers..
    gl.deleteBuffer(this.vertexBuffer);
    gl.deleteBuffer(this.uvBuffer);
    gl.deleteBuffer(this.colorBuffer);
    gl.deleteBuffer(this.indexBuffer);
};
